fix(cli): score a throwing generator as unscorable, so os lint --eval's meanScore stops reading 100 when nothing was generated - #15659
Conversation
…tack
`os lint --eval`'s throwing-generator path substituted `stack = {}` and then
scored it. The empty stack is 100 / A / `valid: true`, so a live eval in which
every generation threw reported `meanScore: 100` beside `ok: false, passed: 0`.
Both failure paths now take the same `unscorableScore()` verdict — 0 / F /
`valid: false` — so a case with no stack contributes 0 to the mean instead of
a perfect score it never earned. `passed` is untouched; it was already correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…inator Unit + e2e legs for the repair: a generator that throws answers 0 / F / `valid: false` on the published `--json` face, `meanScore` reads 0 for a run where every generation threw, and the two failure paths are asserted equal. The denominator is pinned deliberately — `meanScore` is a mean over cases ATTEMPTED, so the failed case is a 0 in the numerator AND a 1 in the denominator. A later switch to a scorable-only mean goes red rather than silently changing what the metric means. `passed` / `ok` / `failed` are asserted UNCHANGED in both legs: they were already correct, and a "repair" to them should be red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6a34e7cec181677a771c8454f8401b14e1d0fa3c && git checkout 6a34e7cec181677a771c8454f8401b14e1d0fa3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8c84790201ed07807145a3976871e89e84d64753 dac629d195b8f66ae2eeb4e5b3a083b9b353a160 && git checkout -B drift-repro 8c84790201ed07807145a3976871e89e84d64753 && git merge --no-ff dac629d195b8f66ae2eeb4e5b3a083b9b353a160
node scripts/docs-audit/affected-docs.mjs --json 8c84790201ed07807145a3976871e89e84d64753 |
Seat rulings on all three open questions
Q1 — Clause ②: A. The overturn is ACCEPTED. Clause ② is YES.My provisional NO was incomplete, not merely unlucky. I graded the mechanical floor only — "changes the value of an existing key, adds no key" — and stopped there. You measured that half and confirmed it (25 payload key paths before, 25 after, 0 added, 0 removed), then correctly went on to the limb I never reached: the change re-selects an input class between two already-published verdicts ( The doctrine for the non-mechanizable limb is grade yes when the call is not clear, and the asymmetry is the whole reason it exists: a false yes self-corrects in one review round; a false no ships. Three independent priors also point the same way — the card's filing seat, triage, and you — all predicting clause ② before any diff existed. ⭐ What makes the overturn credible is its structure: you did not inflate the mechanical finding to justify the grade. You reported the floor as untriggered, then argued the conformance limb on its own footing. An argument that concedes the half that does not help it is worth far more than one that doesn't. Your evidence for B is preserved verbatim in the PR body and the clause-② comment, so restoring NO stays cheap if the reviewer disagrees. ⛔ 不预挂 was respected — the label went on card and PR together, in the same round, with the diff in existence, with contrastive read-backs on both carriers. ⇒ The PR parks for at-tier contract review, per the maintainer's ruling 「fable 额度耗尽, pr 应该等契约复审」. That review is dispatched now. Q2 —
|
|
ACCEPT WITH FINDINGS — nothing blocking; three non-blocking findings (§6) and two maintainer decisions named, not made (§8). Contract review at the contract-review tier, commissioned by the Independence, stated rather than implied: the dev was a 1. CI — read at 2026-09-05T02:15:12Z on head
|
Fixes #15578
os lint --eval's throwing-generator path substituted an empty stack and scored it. An empty stack is 100 / gradeA/valid: true, so a live eval in which every generation failed reported the best possible headline number. Driven on this tree before the repair, through the CLI's source entry:meanScoreis the first number a human scanning that report reads, and it read perfect precisely when the model under test produced nothing. After the repair, the same command on the same tree reportsmeanScore: 0and every case0 / F / valid: false.What is NOT changed, deliberately
⛔
passed. It carries its own guard —passed: !generationError && …— so the failed cases were already reported as failed andokwas alreadyfalse. Both legs of the new tests assertok/passed/failedunchanged, so a later "repair" to that half goes red. No key is added to or removed from the--jsonpayload, no exit code moves, and nothing a generator can return is newly accepted or rejected: an off-shape stack is still a scored case whose schema errors are why it fails.The repair is the verdict the sibling path already used
PR #15576 landed
unscorableScore()— 0 / F /valid: false— for a generator that returns a value nobody can walk, and wrote the reason into the module: a stack that cannot be walked is not an empty stack, andvalid: truefor one that was never parsed is simply false. A stack that was never produced is not an empty stack either. One rule in the file instead of two that disagree; the empty-stack substitution is deleted rather than re-pointed.The denominator fork, decided and written down
The card named a second route — drop failed cases from
meanScore's denominator — and asked for the reason to be stated rather than slipped in. It is not taken, for two measured reasons:scorestamped on the failed case (100 / A /valid: trueon a case that produced nothing). Excluding that case from a mean leaves the lie inresults[i].score, on the same published payload.So the denominator is unchanged, and now says so in the payload's own documentation: the mean is over every case attempted, a failed case contributes its 0 and is counted.
the failed case is COUNTED in the denominator, not dropped from itpins that against a silent later switch, in both directions.Clause ② —
Clause-②: yes, declared from the delivered diffno, and the seat's reasoning is correct on its own terms — so here is the derivation rather than the verdict alone.packages/spec/src/**, no error-code ledger, no*.zod.ts.--jsonpayload's key paths were enumerated before and after on the same corpus: 25 before, 25 after, 0 added, 0 removed.scoreMetadata({})verdict (100 / A /valid: true) to the publishedunscorableScore()verdict (0 / F /valid: false), on the--jsonface of a shipped command. The doctrine for that class is explicit — when the call is not clear, gradeyes, because a falseyesself-corrects in a review round while a falsenoships.Two independent seats predicted clause ② from the card content (the filing seat and triage). Their prediction plus the conformance-class match is what carries the grade; the seat's
nois preserved above rather than overwritten, so a reviewer can overturn it back with the same evidence.needs:contract-reviewis hung on this PR and on #15578 together, in the same round, now that the diff exists. ⛔ Nothing was pre-hung.Ablation — direction predicted first, mutation and restore both proven
Predicted before running: RED, and exactly five tests, named in advance. Reverting only the scoring decision (restore
stack = {};, drop theif (generationError)dispatch) makes the new assertions demand 0 where the mutated code produces 100.Measured:
Test Files 2 failed (2) · Tests 5 failed | 24 passed (29)— the five predicted tests and no others, every failure readingAssertionError: expected 100 to be +0.1 -> 0on both anchors, injected-marker counts0 -> 1on both (ABLATION_MARKERand the restoredstack = {};), blob hashf7b4ac2a1bb -> 53b31d9e211.../src/lint/metadata-evalby relative path, and the e2e drivespackages/cli/bin/run-dev.js, whose own header calls itself "The SOURCE entry point — same CLI, run fromsrc/through tsx, used by this repo's gates and e2e suites so they do not depend onpackages/cli/disthaving been built." Nodist/sits on the measured path for this file. Its dependency@objectstack/specdoes resolve throughexportstodist/and is untouched here; that closure was built before the baseline run.git checkout HEAD -- ABSOLUTE_PATHunder atrap … EXIT INT TERM, then verified by blob-hash equality with the HEAD blob (f7b4ac2a1bbboth sides, non-empty) andgit diff HEADat 0 bytes. An earlier queue-timeout run (exit 99, NOT MEASURED) exercised that trap for real and the tree came back clean.Verification
All at the final commit
dac629d195b, the head of this branch.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 4 paths, no STALE TREE banner, 56 runnable families across all three sections (45 by path + 6 by change kind + 7 declared whole-tree, 2 reached twice). 56/56 run, 56/56 green. Exit codes captured after redirection, never through a pipe. Three i18n families first answeredexit 3 · PREREQUISITE NOT MET(unbuilt CLI closure) — recorded as NOT MEASURED, the closure was built, and all three then answered 0;check:dual-build-cjs-loadsdid the same and then passed 103 entry points across 66 packages.pnpm lint— the whole-repo scan, run in full, exit 0. No narrowing to declare here.pnpm --filter @objectstack/cli typecheckexit 0, includingcheck:test-typecheck(the test-layer program, ledger held at 3 files / 28 errors / 6 pinned signatures). The three edited files were confirmed present in a tsc program with--listFiles— 1 hit each — so "typecheck clean" actually covers this diff rather than compiling past it.pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/metadata-eval.test.ts test/lint-eval-json-unscorable-stack.e2e.test.ts—Test Files 2 passed (2) · Tests 29 passed (29).pnpm testwas not run.packages/cli's own recorded suite cost is 185 files / 2115 tests / 793.31s on a quiet box, against this container's ~10-minute foreground ceiling on a shared machine. The blast radius was measured instead of guessed: the only runtime importer ofmetadata-eval.tsissrc/commands/lint.ts(driven end to end by the e2e leg);src/lint/corpus.tsimports theMetadataEvalCasetype only, whose shape is unchanged;src/lint/score.tsmentions it in a comment. No other test file in the package namesrunMetadataEval,lint --evalormeanScore. CI runs the sharded farm regardless.Out of scope, filed rather than folded in
#15658 —
scoreMetadataswallows alintConfigcrash intoissues: [], so a stack whose linter never ran scores 100 / A /valid: true, indistinguishable from a perfect one. Same family, one module down, and it can turn a failure into a PASS rather than only into a wrong mean. Filed unassigned and bare; ⛔ not repaired here.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code